package bali;

import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.lang.annotation.Inherited;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import javax.tools.FileObject;

import freemarker.template.Configuration;
import freemarker.template.SimpleHash;
import freemarker.template.SimpleSequence;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModelException;

/**
 * Generate code for particularly annotated classes,
 * from a freemarker template which uses the standard java language model.
 * 
 * @author josh
 *
 */
public abstract class SimpleFreemarkerAnnotationProcessor extends AbstractProcessor {
	Configuration configuration;
	BufferedWriter singleFile;
	ProcessingEnvironment processingEnv;

	protected abstract Class<? extends Annotation> getAnnotationClass();

	protected String getGeneratedName(TypeElement element) {
		return element.getQualifiedName() + getAnnotationClass().getSimpleName();
	}

	protected Class<? extends Annotation>[] getSiblingAnnotationClasses() {
		return new Class[] {};
	}

	protected Class<? extends Annotation>[] getNestedAnnotationClasses() {
		return new Class[] {};
	}

	public SimpleFreemarkerAnnotationProcessor() {
		configuration = new Configuration();
		configuration.setClassForTemplateLoading(getAnnotationClass(), "");
	}
	
	@Override
	public SourceVersion getSupportedSourceVersion() {
		return SourceVersion.RELEASE_6;
	}
	
	@Override
	public Set<String> getSupportedAnnotationTypes() {
		return Collections.singleton(getAnnotationClass().getName());
	}

	public void init(ProcessingEnvironment processingEnv) {
		this.processingEnv = processingEnv;
	}

	protected Map<String, Object> getAnnotationParameters(Element annotatedElement, Class clz) {
		Map<String, Object> hash = new HashMap();
		for (AnnotationMirror am : annotatedElement.getAnnotationMirrors()) {
			if (am.getAnnotationType().toString().equals(clz.getCanonicalName())) {
				Map<? extends Element, ? extends AnnotationValue> parameters = am.getElementValues();
				for(Element parameter : parameters.keySet()) {
					String key = parameter.getSimpleName().toString();
					Object value = parameters.get(parameter).getValue();
					if (value instanceof DeclaredType) {
						value = ((DeclaredType)value).asElement();
					}
					hash.put(key, value);
				}
				return hash;
			}
		}
		return hash;
	}

	@Override
	public boolean process(Set<? extends TypeElement> arg0, RoundEnvironment roundEnv) {
		Map<String, SimpleSequence> model = getModel(roundEnv);
		writeTemplate(model);
		return true;
	}

	private Map<String, SimpleSequence> getModel(RoundEnvironment roundEnv) {
		Map<String, SimpleSequence> model = new HashMap<String, SimpleSequence>();
		Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(getAnnotationClass());
		for (Element annotatedElement : elementsAnnotatedWith) {
			if (annotatedElement.getKind() == ElementKind.CLASS) {
				TypeElement classElement = (TypeElement) annotatedElement;
				String file = getGeneratedName(classElement);
				if (!model.containsKey(file)) {
					model.put(file, new SimpleSequence());
				}
				SimpleHash hash = new SimpleHash();
				model.get(file).add(hash);
				hash.put("class", annotatedElement);
				hash.put("annotation", getAnnotationParameters(annotatedElement, getAnnotationClass()));
				for (Element enclosedElement : classElement.getEnclosedElements()) {
					for (Class<? extends Annotation> annotationType : getNestedAnnotationClasses()) {
						if (enclosedElement.getAnnotation(annotationType) != null) {
							String hashKey = annotationType.getSimpleName();
							if (!hash.containsKey(hashKey)) {
								hash.put(annotationType.getSimpleName(), new SimpleSequence());
							}
							try {
								((SimpleSequence)hash.get(annotationType.getSimpleName())).add(enclosedElement);
							} catch (TemplateModelException e) {
								continue;
							}
						}
					}
				}
				for (Class<? extends Annotation> annotationType : getSiblingAnnotationClasses()) {
					for (Element sibling : roundEnv.getElementsAnnotatedWith(annotationType)) {
						Map<String, Object> params = getAnnotationParameters(sibling, annotationType);
						if (params.get("value").toString().equals(annotatedElement.toString())) {
							String hashKey = annotationType.getSimpleName();
							if (!hash.containsKey(hashKey)) {
								hash.put(annotationType.getSimpleName(), new SimpleSequence());
							}
							try {
								((SimpleSequence)hash.get(annotationType.getSimpleName())).add(sibling);
							} catch (TemplateModelException e) {
								continue;
							}
						}
					}
				}
				System.err.println("writing "+hash+" to "+file);
			}
		}
		return model;
	}

	private void writeTemplate(Map<String, SimpleSequence> model) {
		for (String file : model.keySet()) {
			try {
				JavaFileObject jfo = processingEnv.getFiler().createSourceFile(file);
				Template template = configuration.getTemplate(getAnnotationClass().getSimpleName()+".ftl");
				BufferedWriter bw = new BufferedWriter(jfo.openWriter());
				SimpleHash hash = new SimpleHash();
				hash.put("bali", model.get(file));
				hash.put("Elements", processingEnv.getElementUtils());
				template.process(hash, bw);
				bw.close();
			} catch (TemplateException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
